package genindex

import (
	"bufio"
	"fmt"
	"markdown_gen_index/src/util"
	"os"
	"path"
	"path/filepath"
	"slices"
	"strings"

	"github.com/go-git/go-git/plumbing/format/gitignore"
)

func getSubExcludes(subDir string) []string {
	var result []string
	// sub .mdignore
	result = append(result, getIgnoreEntry(path.Join(subDir, ".mdignore"))...)
	// sub .gitignore
	result = append(result, getIgnoreEntry(path.Join(subDir, ".gitignore"))...)
	return result
}

func (idx *index) getIndexNav() string {
	if len(idx.chains) <= 1 {
		return ""
	}

	var indexNav string
	// index not included, so loop to len-1
	for i := 0; i < len(idx.chains)-1; i++ {
		title := util.If(i == 0, idx.homeTitle, idx.chains[i].title)
		backpath := strings.Repeat("../", len(idx.chains)-i-1) + path.Base(idx.chains[i].file)
		indexNav += fmt.Sprintf("[%s](%s) / ", title, getLink(backpath))
	}
	indexNav += idx.title + "\n\n"
	return indexNav
}

func (idx *index) getEntryNavPrefix() string {
	var navPrefix string
	for i := 0; i < len(idx.chains); i++ {
		title := util.If(i == 0, idx.homeTitle, idx.chains[i].title)
		backpath := strings.Repeat("../", len(idx.chains)-i-1) + path.Base(idx.chains[i].file)
		navPrefix += fmt.Sprintf("[%s](%s) / ", title, getLink(backpath))
	}
	return navPrefix
}

func (idx *index) decorateEntry() {
	for _, entry := range idx.entries {
		if s, _ := filepath.Rel(idx.file, entry.file); s == "." {
			continue
		}
		b, err := os.ReadFile(entry.file)
		if err == nil {
			if len(b) == 0 {
				continue
			}

			lines := strings.Split(string(b), "\n")

			navPrefix := idx.getEntryNavPrefix()
			if strings.HasPrefix(lines[0], "[") {
				// update nav
				lines[0] = navPrefix + readTitle(entry.file)
			} else {
				// insert nav
				lines = append([]string{navPrefix + readTitle(entry.file) + "\n"}, lines...)
			}

			if len(lines) > 4 {
				if lines[len(lines)-3] == "---" {
					lines = lines[:len(lines)-3]
				}
				if lines[len(lines)-5] == "---" {
					lines = lines[:len(lines)-5]
				}
			}

			// update bottom nav
			bottomNav := entry.getBottomNav()
			if bottomNav != "" {
				lines = append(lines, bottomNav)
			}

			updated := strings.Join(lines, "\n")
			f, err := os.Create(entry.file)
			if err == nil {
				defer f.Close()
				f.WriteString(updated)
			}
		}
	}
}

func (e *entry) getBottomNav() string {
	var result string
	if e.prev != nil {
		result += fmt.Sprintf("[<< %s](%s)\n", e.prev.title, getLink(path.Base(e.prev.file)))
	}
	if e.next != nil {
		if result != "" {
			result += "\n"
		}
		result += fmt.Sprintf("[>> %s](%s)\n", e.next.title, getLink(path.Base(e.next.file)))
	}
	if result != "" {
		result = "---\n" + result
	}
	return result
}

func parseContent(idx *index, opt *parseContentOption) string {
	for _, subIdx := range idx.children {
		relPath, _ := filepath.Rel(opt.WorkDir, subIdx.file)
		if opt.Depth == 0 {
			if opt.NoHeaderLink {
				opt.Content += fmt.Sprintf("\n## %s\n", subIdx.title)
			} else {
				opt.Content += fmt.Sprintf("\n## [%s](%s)\n", subIdx.title, getLink(relPath))
			}
		} else {
			opt.Content += fmt.Sprintf("\n%s- [%s](%s)", strings.Repeat("  ", opt.Depth-1), subIdx.title, getLink(relPath))
		}

		opt.Content += parseContent(subIdx, &parseContentOption{
			WorkDir:      opt.WorkDir,
			Content:      "",
			Depth:        opt.Depth + 1,
			NoHeaderLink: opt.NoHeaderLink,
		})
	}

	for _, entry := range idx.entries {
		relPath, _ := filepath.Rel(opt.WorkDir, entry.file)
		if opt.Depth == 0 {
			opt.Content += fmt.Sprintf("\n[%s](%s)\n", entry.title, getLink(relPath))
		} else {
			opt.Content += fmt.Sprintf("\n%s- [%s](%s)", strings.Repeat("  ", opt.Depth-1), entry.title, getLink(relPath))
		}
	}

	if !strings.HasSuffix(opt.Content, "\n") {
		opt.Content += "\n"
	}

	return opt.Content
}

func getLink(file string) string {
	return strings.ReplaceAll(file, " ", "%20")
}

func hasMdFile(dir, indexFile string) bool {
	if v, ok := dirHasMdFileMap[dir]; ok {
		return v
	}

	subExcludes := getSubExcludes(dir)

	dirEntries, _ := os.ReadDir(dir)
	for _, de := range dirEntries {
		if matchFile(subExcludes, de.Name()) {
			continue
		}
		if !de.IsDir() {
			if slices.Contains(mdExts, path.Ext(de.Name())) && de.Name() != indexFile {
				dirHasMdFileMap[path.Join(dir, de.Name())] = true
				dirHasMdFileMap[dir] = true
				return true
			}
		} else {
			if hasMdFile(path.Join(dir, de.Name()), indexFile) {
				dirHasMdFileMap[dir] = true
				return true
			}
		}
	}

	return dirHasMdFileMap[dir]
}

func matchFile(paths []string, file string) bool {
	patterns := []gitignore.Pattern{}

	for _, p := range paths {
		patterns = append(patterns, gitignore.ParsePattern(p, nil))
	}
	m := gitignore.NewMatcher(patterns)
	return m.Match(strings.Split(file, "/"), true)
}

func readTitle(file string) string {
	if v, ok := fileTitleMap[file]; ok {
		return v
	}

	if !slices.Contains(mdExts, path.Ext(file)) {
		fileTitleMap[file] = path.Base(file)
		return fileTitleMap[file]
	}

	if _, err := os.Stat(file); os.IsNotExist(err) {
		fileTitleMap[file] = path.Base(path.Dir(file))
		return fileTitleMap[file]
	}

	f, err := os.Open(file)
	if err != nil {
		fileTitleMap[file] = path.Base(file)
		return fileTitleMap[file]
	}
	defer f.Close()

	s := bufio.NewScanner(f)
	for s.Scan() {
		cut, ok := strings.CutPrefix(s.Text(), "# ")
		if ok && len(cut) > 0 {
			fileTitleMap[file] = strings.TrimSpace(cut)
			return fileTitleMap[file]
		}
	}
	fileTitleMap[file] = path.Base(file)
	return fileTitleMap[file]
}

func getIgnoreEntry(ignoreFile string) []string {
	var result []string
	mdiignore, err := os.Stat(ignoreFile)
	if err == nil && !mdiignore.IsDir() {
		f, err := os.Open(ignoreFile)
		if err == nil {
			defer f.Close()
			s := bufio.NewScanner(f)
			for s.Scan() {
				line := strings.TrimSpace(s.Text())
				if line != "" && !strings.HasPrefix(line, "#") {
					result = append(result, s.Text())
				}
			}
		}
	}
	return result
}
